#include "gmk_api.h"
#include <assert.h>
#include <Python.h>
#include <limits.h>
#include <stdlib.h>

#if PY_VERSION_HEX < 0x030A0000
#error Python version MUST be newer than 3.9
#endif
int            _gnumake_gmk_setup(void);
bool           gmk_debug = true;
PyMODINIT_FUNC PyInit__gnumake(void);

/**
 * 查找命令的可执行文件路径
 * @param command 要查找的命令
 * @return 返回可执行文件的完整路径，如果未找到则返回NULL
 */
static char *which(const char *command)
{
    char *path_env = getenv("PATH");
    if (path_env == NULL)
    {
        return NULL;
    }

    // 复制PATH环境变量，避免修改原始字符串
    char *path = strdup(path_env);
    if (path == NULL)
    {
        return NULL;
    }

    char *token = strtok(path, ":");
    while (token != NULL)
    {
        // 构建完整路径
        char *full_path = malloc(strlen(token) + strlen(command) + 2); // +2 for '/' and '\0'
        if (full_path == NULL)
        {
            free(path);
            return NULL;
        }

        sprintf(full_path, "%s/%s", token, command);

        // 检查文件是否存在且可执行
        struct stat st;
        if (stat(full_path, &st) == 0 && (st.st_mode & S_IXUSR))
        {
            free(path);
            return full_path;
        }

        free(full_path);
        token = strtok(NULL, ":");
    }

    free(path);
    return NULL;
}
/* From the Python perspective, this is just a dummy module: there's
 * actually nothing here. It exists as a Python module so that we can
 * get its __file__ attribute easily.
 *
 * Its real purpose is to be loaded into GNU make, where it will start up
 * the Python interpreter and load the real gnumake module.
 */
static struct PyModuleDef pygnumake_module = {
    PyModuleDef_HEAD_INIT,
    "gnumake._gnumake",
    "Internal extension module for gnumake.\n\n"
    "Load this module into make with the load directive.\n"
    "It has no user-accessible functions."
        - 1,
};

/** @brief Python module init function
 *
 * Called when imported by Python
 */
PyMODINIT_FUNC PyInit__gnumake(void)
{
    PyObject *mod = NULL;

    mod = PyModule_Create(&pygnumake_module);

    return mod;
}

/** Required by GNU make */
int plugin_is_GPL_compatible;

/**
 * @brief Configuration used to initialize Python
 */
static PyConfig gnumake_config;

/** @brief Clean up Python on exit
 *
 * GNU make provides no other way to clean up except an atexit handler
 */
static void pygnumake_gmk_cleanup(void)
{
    PyGILState_Ensure(); // make it safe to run Python
    PyConfig_Clear(&gnumake_config);
    Py_Finalize();
}

/** @brief Helper function to set an environment variable from a GNU make
 *  variable.
 *
 *  Needed because even if you mark a variable as exported, it doesn't make it
 *  into the environment until recipes are executed.
 *
 *  @param env_name Name of destination environment variable
 *  @param make_name Name of the source make variable
 */
static void export_var(const char *env_name, const char *make_name)
{
    char *value;
    char  buffer[256];
    int   ret;

    ret = snprintf(buffer, sizeof(buffer), "$(strip $(%s))", make_name);
    if (ret < 0 || (size_t)ret >= sizeof(buffer))
    {
        return;
    }

    value = gmk_api.expand(buffer);
    if (value)
    {
        if (value[0])
        {
            setenv(env_name, value, 1);
        }
        gmk_api.free(value);
    }
}

/** @brief Helper function to set an environment variable from a GNU make
 *  variable.
 *
 *  As export_var, but appends to an existing variable and joins using a
 *  given separator.
 *
 *  @param env_name Name of destination environment variable
 *  @param make_name Name of the source make variable
 *  @param join     The char to use to join list elements.
 */
static void append_var_list(const char *env_name, const char *make_name, char join)
{
    char *value;
    char *existing;
    char  buffer[256];
    int   ret;
    char *c;

    ret = snprintf(buffer, sizeof(buffer), "$(strip $(%s))", make_name);
    if (ret < 0 || (size_t)ret >= sizeof(buffer))
    {
        return;
    }

    value = gmk_api.expand(buffer);
    if (value)
    {
        if (value[0])
        {
            for (c = value; *c; c++)
            {
                if (*c == ' ')
                {
                    *c = join;
                }
            }

            existing = getenv(env_name);
            if (existing && existing[0])
            {
                size_t existing_len = strlen(existing);
                size_t value_len    = strlen(value);
                size_t needed       = existing_len + value_len + 2;
                char  *combined     = malloc(needed);
                if (combined)
                {
                    memcpy(combined, existing, existing_len);
                    combined[existing_len] = join;
                    memcpy(combined + existing_len + 1, value, value_len);
                    combined[needed - 1] = '\0';
                    setenv(env_name, combined, 1);
                    free(combined);
                }
            }
            else
            {
                setenv(env_name, value, 1);
            }
        }
        gmk_api.free(value);
    }
}

/** @brief Set up environment variables used by Python.
 *
 * This allows the user to control the startup and execution of Python using
 * make variables.
 *
 */
static void pygmk_setup_env(void)
{
    export_var("PYTHONHOME", ".PYTHONHOME");
    append_var_list("PYTHONPATH", ".PYTHONPATH", ':');
    export_var("PYTHONOPTIMIZE", ".PYTHONOPTIMIZE");
    export_var("PYTHONDEBUG", ".PYTYONDEBUG");
    export_var("PYTHONDONTWRITEBYTECODE", ".PYTHONDONTWRITEBYTECODE");
    export_var("PYTHONINSPECT", ".PYTHONINSPECT"); // does this work?
    export_var("PYTHONIOENCODING", ".PYTHONIOENCODING");
    export_var("PYTHONUSERSITE", ".PYTHONUSERSITE");
    export_var("PYTHONUNBUFFERED", ".PYTHONUNBUFFERED");
    export_var("PYTHONVERBOSE", ".PYTHONVERBOSE");
    append_var_list("PYTHONWARNINGS", ".PYTHONWARNINGS", ',');
    export_var("PYTHONHASHSEED", ".PYTHONHASHSEED");
}
static char pygmk_init_script[] = "import ctypes\n"
                                  "import sys\n"
                                  "import os.path as op\n"
                                  "libdl = ctypes.CDLL(None)\n"
                                  "class Dl_info(ctypes.Structure):\n"
                                  "    _fields_ = [\n"
                                  "        ('dli_fname', ctypes.c_char_p),\n"
                                  "        ('dli_fbase', ctypes.c_void_p),\n"
                                  "        ('dli_sname', ctypes.c_char_p),\n"
                                  "        ('dli_saddr', ctypes.c_void_p),\n"
                                  "    ]\n"
                                  "libdl.dladdr.argtypes = [ctypes.c_void_p, ctypes.POINTER(Dl_info)]\n"
                                  "libdl.dladdr.restype = ctypes.c_int\n"
                                  "def get_symbol_path(symbol):\n"
                                  "    info = Dl_info()\n"
                                  "    address = ctypes.cast(libdl[symbol], ctypes.c_void_p).value\n"
                                  "    if libdl.dladdr(address, ctypes.byref(info)) != 0:\n"
                                  "        return info.dli_fname.decode('utf-8') if info.dli_fname else None\n"
                                  "    return None\n"
                                  "try:\n"
                                  "    import gnumake\n"
                                  "    del sys.modules['gnumake']\n"
                                  "except ImportError:\n"
                                  "    pygmk_path = get_symbol_path('_gnumake_gmk_setup')\n"
                                  "    if pygmk_path is None:\n"
                                  "        raise ImportError('GNU make not detected')\n"
                                  "    pygmk_path = op.dirname(op.abspath(pygmk_path))\n"
                                  "    sys.path.insert(0, op.join(pygmk_path, '..'))\n"
                                  "    import gnumake\n"
                                  "    del sys.modules['gnumake']";
static int pygmk_init_python(char *program_name)
{
    int       ret     = -1;
    PyObject *gnumake = NULL;
    PyStatus  status;

    pygmk_setup_env();

    PyConfig_InitPythonConfig(&gnumake_config);

    status = PyConfig_SetBytesString(&gnumake_config, &gnumake_config.program_name, program_name);

    if (PyStatus_Exception(status))
    {
        GMK_DEBUG("Failed to set program name: %s", status.err_msg);
        goto exception;
    }

    status = Py_InitializeFromConfig(&gnumake_config);
    if (PyStatus_Exception(status))
    {
        GMK_DEBUG("Failed to initialize Python: %s", status.err_msg);
        goto exception;
    }

    if (atexit(pygnumake_gmk_cleanup) != 0)
    {
        PyErr_SetFromErrno(PyExc_OSError);
        goto exception;
    }

    PyRun_SimpleString(pygmk_init_script);
    gnumake = PyImport_ImportModule("gnumake");
    if (!gnumake)
    {
        GMK_DEBUG("Failed to import gnumake");
        goto exception;
    }

    ret = 0;

exception:
    if (PyStatus_Exception(status))
    {
        PyConfig_Clear(&gnumake_config);
    }

    if (PyErr_Occurred())
    {
        // The user will probably want to know.
        PyErr_Print();
    }

    Py_XDECREF(gnumake);

    // Release the GIL
    PyEval_SaveThread();
    return ret;
}
/** @brief Initialize Python for GNU make
 *
 * Called when this .so is loaded into GNU make with the load directive. This
 * will cause PyInit__gnumake() to be called as well when we load the
 * appropriate module
 *
 * @return 1 on success, -1 on error
 */
int _gnumake_gmk_setup(void)
{
    int   ret            = 0;
    char *python_program = NULL;
    if (load_gmk_api() < 0)
    {
        GMK_DEBUG("Failed to load GNU Make API");
        goto done;
    }
    gmk_debug = getenv("PYGMK_DEBUG") == NULL ? false : true;

    python_program = which("python");
    if (python_program == NULL)
    {
        python_program = which("python3");
        if (python_program == NULL)
        {
            fprintf(stderr, "ERROR: python not found in PATH\n");
            goto done;
        }
    }

    ret = pygmk_init_python(python_program);
    GMK_DEBUG("pygmk_init_python() returned %d\n", ret);

done:
    if (python_program)
    {
        free(python_program);
    }
    return ret == 0 ? 1 : 0;
}
